﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using PDO.Utilities;

namespace PDO.DataPersistence.Core
{
    public enum Operation
    {
        Update,
        Insert,
        Remove
    }

    public class Memento
    {
        private class MementoItem
        {
            IPersistableData _pdo;
            Operation _operation;
            PropertyInfo _propertyInfo;
            object _oldValue;
            object _newValue;
            int _position;

            public IPersistableData Pdo
            {
                get { return _pdo; }
                set { _pdo = value; }
            }

            internal Operation Operation
            {
                get { return _operation; }
                set { _operation = value; }
            }

            public PropertyInfo PropertyInfo
            {
                get { return _propertyInfo; }
                set { _propertyInfo = value; }
            }

            public object OldValue
            {
                get { return _oldValue; }
                set { _oldValue = value; }
            }

            public object NewValue
            {
                get { return _newValue; }
                set { _newValue = value; }
            }

            public int Position
            {
                get { return _position; }
                set { _position = value; }
            }
        }

        private object _syncRoot;
        private int _ownerThreadId;

        private List<MementoItem> _items;
        private bool _recordingEnabled;

        internal bool RecordingEnabled
        {
            get { return _recordingEnabled; }
            set { _recordingEnabled = value; }
        }

        private Memento()
        {
        }

        public Memento(int ownerThreadId)
            : this()
        {
            _ownerThreadId = ownerThreadId;
            _items = new List<MementoItem>();
            _syncRoot = new object();
            _recordingEnabled = true;
        }

        public void Record(IPersistableData pdo, Operation operation, string propertyName, object oldValue, object newValue)
        {
            if (!_recordingEnabled)
                return;

            if (System.Threading.Thread.CurrentThread.ManagedThreadId != _ownerThreadId)
                return;

            PropertyInfo info = ReflectionHelper.GetPropertyInfo(pdo, propertyName);
            _items.Add(new MementoItem() { Pdo = pdo, Operation = operation, PropertyInfo = info, OldValue = oldValue, NewValue = newValue });
        }

        public void Record(IPersistableData pdo, Operation operation, object oldValue, int position)
        {
            if (!_recordingEnabled)
                return;

            if (System.Threading.Thread.CurrentThread.ManagedThreadId != _ownerThreadId)
                return;

            _items.Add(new MementoItem() { Pdo = pdo, Operation = operation, OldValue = oldValue, Position = position });
        }

        public bool HasChanges
        {
            get
            {
                return _items.Count > 0;
            }
        }

        public void Undo()
        {
            using (TimedLock.Lock(_syncRoot))
            {
                _recordingEnabled = false;
                MementoItem[] a = _items.ToArray();
                for (int i = a.Length - 1; i >= 0; i--)
                {
                    MementoItem m = a[i];
                    switch (m.Operation)
                    {
                        case Operation.Update:
                            m.PropertyInfo.SetValue(m.Pdo, m.OldValue, null);
                            break;

                        case Operation.Insert:
                            {
                                IPersistableDataObjectList pdoList = m.Pdo as IPersistableDataObjectList;
                                pdoList.Remove(m.OldValue);
                                break;
                            }

                        case Operation.Remove:
                            {
                                IPersistableDataObjectList pdoList = m.Pdo as IPersistableDataObjectList;
                                pdoList.Insert(m.Position, m.OldValue);
                                break;
                            }
                    }
                }
                _recordingEnabled = true;
            }
        }

        public void Redo()
        {
            using (TimedLock.Lock(_syncRoot))
            {
                _recordingEnabled = false;
                MementoItem[] a = _items.ToArray();
                for (int i = 0; i < a.Length; i++)
                {
                    MementoItem m = a[i];
                    m.PropertyInfo.SetValue(m.Pdo, m.NewValue, null);
                }
                _recordingEnabled = true;
            }
        }
    }

    public static class MementoManager
    {
        static Stack<Memento> _mementoStack;
        static Memento _activeMemento;

        private static Stack<Memento> MementoStack
        {
            get
            {
                if (null == _mementoStack)
                {
                    _mementoStack = new Stack<Memento>();
                }
                return _mementoStack;
            }
        }

        public static Memento CreateMemento(PersistableDataObject rootParent)
        {
            Memento m = new Memento(System.Threading.Thread.CurrentThread.ManagedThreadId);
            _activeMemento = m;
            MementoStack.Push(m);
            return m;
        }

        public static void ReleaseMemento(Memento m)
        {
            if (m != MementoStack.Peek())
            {
                throw new InvalidOperationException("Attempted to release a memento out of proper order.");
            }

            MementoStack.Pop();

            if (MementoStack.Count == 0)
            {
                _activeMemento = null;
            }
            else
            {
                _activeMemento = MementoStack.Peek();
            }

        }

        public static Memento ActiveMemento
        {
            get { return _activeMemento; }
        }

        public static bool RecordingEnabled
        {
            get
            {
                if (_activeMemento != null)
                    return _activeMemento.RecordingEnabled;
                else
                    return false;
            }
            set
            {
                if (_activeMemento != null)
                    _activeMemento.RecordingEnabled = value;
            }
        }
        public static void Record(PersistableDataObject pdo, string propertyName, object oldValue, object newValue)
        {
            if (null != _activeMemento)
            {
                _activeMemento.Record(pdo, Operation.Update, propertyName, oldValue, newValue);
            }
        }

        public static void Record(IPersistableDataObjectList pdo, Operation operation, object newValue)
        {
            Record(pdo, operation, newValue, -1);
        }

        public static void Record(IPersistableDataObjectList pdo, Operation operation, object newValue, int position)
        {
            if (null != _activeMemento)
            {
                _activeMemento.Record(pdo, operation, newValue, position);
            }
        }

        public static void Undo()
        {
            if (null != _activeMemento)
            {
                _activeMemento.Undo();
            }
        }

        public static void Redo()
        {
            if (null != _activeMemento)
            {
                _activeMemento.Redo();
            }
        }

    }
}
